var GearsObjectStore = Class.create(ObjectStore, {
    initialize: function($super, storeKey)
    {
        $super(storeKey);

        if (!(window.google && google.gears))
            throw "In order to use ObjectThought, Google Gears must be installed.";
    },
    
    createTransaction: function()
    {
        return {
            store: this,
            context: this.context,
            
            transactionStarted: false,
            
            commands: [],
            
            issue: function(object, dml, arguments, hooks)
            {
                if (!this.transactionStarted)
                {
                    this.store.executeDML("BEGIN TRANSACTION");
                    this.transactionStarted = true;
                }
                
                // collect the command for deferred committing
                this.commands.push({
                    object: object,
                    dml: dml,
                    arguments: arguments,
					hooks: (hooks == null) ? [] : hooks
                });
            },
            
            commit: function()
            {                
                if (!this.transactionStarted)
                    return;
                
                // commit each command
                try
                {
                    this.commands.each(function(command)
                    {
                        var lastInsertIdentifier = this.store.executeDML(command.dml, command.arguments);
                        if (command.hooks.onPostExecute)
							command.hooks.onPostExecute(command.object, lastInsertIdentifier);
                    }, this);
                    
                    if (this.store.__test != null)
                    {
                        if (this.store.__test.simulatedDatabaseErrors != null &&
                            this.store.__test.simulatedDatabaseErrors.onCommit != null &&
                            Object.isFunction(this.store.__test.simulatedDatabaseErrors.onCommit))
                        {
                            this.store.__test.simulatedDatabaseErrors.onCommit();
                        }
                    }
                    
                    this.store.executeDML("COMMIT TRANSACTION");
					
					this.commands.each(function(command)
                    {
						if (command.hooks.onPostCommit)
							command.hooks.onPostCommit(command.object);
					}, this);
                }
                catch(e)
                {
                    console.error(e);
                    this.rollback();
                    throw e;
                }
                finally
                {
                    this.commands = [];
                    this.transactionStarted = false;
                }
            },
            
            rollback: function()
            {
                if (!this.transactionStarted)
                    return;
                
                try
                {   
                    this.store.executeDML("ROLLBACK TRANSACTION");
                    
                    this.commands.each(function(command)
                    {
                        if (command.hooks.onRollback)
                            command.hooks.onRollback(command.object);
                    }, this);
                }
                finally
                {
                    this.commands = [];
                    this.transactionStarted = false;
                }
            }
        };
    },

    ensureDatabase: function()
    {
        if (GearsObjectStore.__database == null)
        {
            console.log("Opening store '" + this.storeKey + "' using Google Gears...");
            
            GearsObjectStore.__database = google.gears.factory.create('beta.database');
            GearsObjectStore.__database.open(this.storeKey);
        }
    },
    
    executeDDL: function(ddlString)
    {
        this.ensureDatabase();
        
        console.log("Executing DDL '" + ddlString + "'...");
        
        try
        {
            GearsObjectStore.__database.execute(ddlString);
        }
        catch(e)
        {
            console.error(e);
            throw e;
        }
    },
    executeDML: function(dmlString, args)
    {
        this.ensureDatabase();
        
        console.log("Executing DML '" + dmlString + "'...");
        if (args != null)
            console.log("  with " + args.inspect());
        
        try
        {            
            GearsObjectStore.__database.execute(dmlString, args);
            return GearsObjectStore.__database.lastInsertRowId;
        }
        catch(e)
        {
            console.error(e);
            throw e;
        }
    },
    executeSQL: function(sqlString, args)
    {
        this.ensureDatabase();

        console.log("Executing SQL '" + sqlString + "'...");
        if (args != null)
            console.log("  with " + args.inspect());
        
        try
        {
            return GearsObjectStore.__database.execute(sqlString, args);
        }
        catch(e)
        {
            console.error(e);
            throw e;
        }
    },
    executeSQLScalar: function(sqlString, args)
    {
        var results = this.executeSQL(sqlString, args);
        try
        {        
            if (results.isValidRow())
                return results.field(0);
            else
                return null;
        }
        finally
        {
            results.close();
        }
    },
    
    getAllocator: function()
    {
        return new GearsObjectStoreAllocator(this);
    },
    allocatedFor: function(classOrClasses)
    {
        var classes = classOrClasses;
        if (!Object.isArray(classes))
            classes = [classes];
        
        // note: we're finding the first **unallocated** class to save time
        var anUnallocatedClass = classes.detect(function(c)
        {
            var tableExistsQuery = "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?";
            return (this.executeSQLScalar(tableExistsQuery, [c.pluralName]) != 1);
        }, this);
        
        return (anUnallocatedClass == null);
    },
    
    
    // CRUD operation implementations
    onSaveObject: function(objectToSave)
    {
        var identityName = objectToSave.constructor.getPersistableIdentifierFieldName();
        var identityValue = objectToSave.getPersistableIdentifier();
        var fieldNames = objectToSave.constructor.getPersistableFieldNames();
        var fieldValues = objectToSave.getPersistableFieldValues();
        
        var newObject = (identityValue == null);
        
        // if the ID is non-null, prepend it to the fieldNames and fieldValues
        if (!newObject)
        {
            fieldNames.unshift(identityName);
            fieldValues.unshift(identityValue);
        }
        
		// Standard metadata fields 
		fieldNames.push("__type__");
		fieldValues.push(objectToSave.constructor.singularName);
		
        // build an INSERT OR REPLACE command for this object
        var saveDML = "INSERT OR REPLACE INTO " + objectToSave.constructor.pluralName + " (";
        saveDML += fieldNames.join(", ");
        saveDML += ") VALUES ("
        saveDML += $R(1, fieldNames.length).map(function() { return "?" }).join(", ");
        saveDML += ")";
		
		var hooks = {};
        
        // for new objects, provide the hooks to manage the identifier field
        if (newObject)
        {
            hooks.onPostExecute = function(object, lastInsertIdentifier)
            {
                object.setPersistableIdentifier(lastInsertIdentifier);
            };
            hooks.onRollback = function(object)
            {
                object.setPersistableIdentifier(null);
            }
        }
        
        // add the command to the queue for this transaction
        this.context.currentTransaction.issue(objectToSave,
                                              saveDML,
                                              fieldValues,
											  hooks);
    },
    
    onGetObject: function(classToRetrieve, objectIdentifier)
    {        
        // build the SELECT for this object
		var className = classToRetrieve.pluralName;
		var getSQL = "SELECT *" + 
					 " FROM " + className + 
					 " WHERE " + classToRetrieve.getPersistableIdentifierFieldName() +
					 " = ?";
		
		// execute and capture the results
		var result = this.executeSQL(getSQL, [objectIdentifier]);
		// create a GearsObjectReader, return the current object
        // or null
		var reader = new GearsObjectReader(this.context, result);
        try
		{
			if (reader.read())
				return reader.current;
			else
	        	return null;
		}
		finally
		{
			reader.close();
		}
		
    },
	
	onGetObjects: function(classToRetrieve)
    {        
        // build the SELECT for this object
		var className = classToRetrieve.pluralName;
		var getSQL = "SELECT *" + 
					 " FROM " + className;
		
		// execute and capture the results
		var result = this.executeSQL(getSQL);
		// create a GearsObjectReader, return the current object
        // or null
		var reader = new GearsObjectReader(this.context, result);
		var toReturn = [];
        try
		{
			while (reader.read())
			{
				toReturn.push(reader.current);
			}
			return toReturn;
		}
		finally
		{
			reader.close();
		}
		
    },
	
	onDeleteObject: function(objectToDelete)
	{
		var identityName = objectToDelete.constructor.getPersistableIdentifierFieldName();
        var identityValue = objectToDelete.getPersistableIdentifier();
		
        // build an DELETE command for this object
        var deleteDML = "DELETE FROM " + objectToDelete.constructor.pluralName + " ";
        deleteDML += "WHERE " + identityName + " = ?";
		
		var hooks = {};
		hooks.onPostCommit = function(objectToNull)
							 {
							 	objectToNull[objectToNull.constructor.getPersistableIdentifierFieldName()] = null;
							 };
        
        // add the command to the queue for this transaction
        this.context.currentTransaction.issue(objectToDelete,
                                              deleteDML,
                                              [identityValue],
											  hooks);
	},
	
	onDeleteObjects: function(classToDelete)
	{
        // build an DELETE command for this object
        var deleteDML = "DELETE FROM " + classToDelete.pluralName;
        
        // add the command to the queue for this transaction
        this.context.currentTransaction.issue(null, deleteDML);
	},
	
	onFindObject: function(classToFind, searchObject)
	{
		var toReturn = this.onFindObjects(classToFind, searchObject);
		return (toReturn.length > 0) ? toReturn[0] : null;
	},
	
	onFindObjects: function(classToFind, searchObject)
	{
		// build the SELECT for this object
		var className = classToFind.pluralName;
		var searchSQL = "SELECT *" + 
					 	" FROM " + className;
		
		// Not using the Gears args here,
		// afraid seperate Arrays might cause problems...
		// could be wrong, can change it back later.
		var queryVars = [];
		var queryValues = [];
		var functionVars = new Hash();
		$H(searchObject).each(function(pair)
		{
			if (Object.isNumber(pair.value) || Object.isString(pair.value))
			{
				queryVars.push(pair.key.concat(" = ?"));
				queryValues.push(pair.value);
			}
			else if (Object.isArray(pair.value) || pair.value instanceof ObjectRange)
			{
				// I need to Learn RegEx...change this when I do.
				var paramList = $A(pair.value).inspect();
				paramList = paramList.slice(1, (paramList.length - 1));
				paramList = "(" + paramList + ")";
				
				queryVars.push(pair.key.concat(" IN ", paramList));
			}
			else if (Object.isFunction(pair.value))
			{
				functionVars.set(pair.key, pair.value);
			}
			else
				throw new Error("SearchObject can only be String, Number, Array, ObjectRange or Function.");
		});
		
		if (queryVars.length > 0)
			searchSQL += " WHERE " + queryVars.join(" AND ");
		
		var result = this.executeSQL(searchSQL, queryValues);
		// create a GearsObjectReader, read objects in an array and return.
		var reader = new GearsObjectReader(this.context, result);
		var toReturn = [];
        try
		{
			while (reader.read())
			{
				var passed = functionVars.all(function(pair) { return pair.value(reader.current[pair.key]) });
				
				if (passed)
					toReturn.push(reader.current);
			}
			return toReturn;
		}
		finally
		{
			reader.close();
		}
	}
});

var GearsObjectStoreAllocator = Class.create({
    store: null,
    
    initialize: function(store)
    {
        this.store = store;
    },
    
    deallocate: function(classesToDeallocate)
    {
        console.log("*******");
        console.log("Deallocating " + classesToDeallocate.length + " classes from store '" + this.store.storeKey + "'...");
        
        classesToDeallocate.each(function(classToDeallocate)
        {
            console.log("Class: " + classToDeallocate.singularName + " (" + classToDeallocate.pluralName + ")");
            this.store.executeDDL("DROP TABLE IF EXISTS " + classToDeallocate.pluralName + ";");
        }, this);
        
        console.log("Finished.");
        console.log("*******");
    },
    
    allocate: function(classesToAllocate)
    {
        console.log("*******");
        console.log("Allocating " + classesToAllocate.length + " classes in store '" + this.store.storeKey + "'...");
        
        classesToAllocate.each(function(classToAllocate)
        {
            console.log("Class: " + classToAllocate.singularName + " (" + classToAllocate.pluralName + ")");
            
            var columnDefinitions = classToAllocate.getPersistableFieldNames().collect(function(fieldName)
            {
                return fieldName;
                
                // TODO: handle special column constraints, etc...
            });
            columnDefinitions.unshift(classToAllocate.getPersistableIdentifierFieldName() + " INTEGER NOT NULL CONSTRAINT PK_" + classToAllocate.pluralName + " PRIMARY KEY AUTOINCREMENT")
            columnDefinitions.push("__type__ TEXT NOT NULL");
            var schemaDDL = "CREATE TABLE IF NOT EXISTS " + classToAllocate.pluralName + " ";
            schemaDDL += "(";
            schemaDDL += columnDefinitions.join(", ");
            schemaDDL += ");";
            
            this.store.executeDDL(schemaDDL);
        }, this);
        
        console.log("Finished.");
        console.log("*******");
    }
});

GearsObjectStore.__database = null;